/**
 * @file drv_soft_i2c.c
 * @brief Software implementation of I2C for 51 microcontroller.
 * @author David Xu
 * @version 1.0.0
 * @date 2023-11-09
 * @note none
 */

#include "drv_soft_i2c.h"
#include "ct_gpio.h"

// sbit SCL = P1 ^ 3;    ///< Serial Clock Line
// sbit SDA = P1 ^ 4;    ///< Serial Data Line

#define CT_SOFT_I2C_SCL_PORT CT_GPIO_PORT1
#define CT_SOFT_I2C_SDA_PORT CT_GPIO_PORT1
#define CT_SOFT_I2C_SCL_PIN  CT_GPIO_PIN_3
#define CT_SOFT_I2C_SDA_PIN  CT_GPIO_PIN_4

#ifndef HIGH
#define HIGH 1
#endif
#ifndef LOW
#define LOW 0
#endif

static void SDA_SetMode(ct_gpio_com_cof_t mode) {
    ct_gpio_SetMode(CT_SOFT_I2C_SDA_PORT, CT_SOFT_I2C_SDA_PIN, mode);
}

static void SCL_SetMode(uint8_t mode) {
    ct_gpio_SetMode(CT_SOFT_I2C_SCL_PORT, CT_SOFT_I2C_SCL_PIN, mode);
}

static void SDA_out(ct_gpio_sta_t state) {
    ct_gpio_Write(CT_SOFT_I2C_SDA_PORT, CT_SOFT_I2C_SDA_PIN, state);
}

static void SCL_out(ct_gpio_sta_t state) {
    ct_gpio_Write(CT_SOFT_I2C_SCL_PORT, CT_SOFT_I2C_SCL_PIN, state);
}

static uint8_t SDA_ReadPin(void) {
    return ct_gpio_Read(CT_SOFT_I2C_SDA_PORT, CT_SOFT_I2C_SDA_PIN);
}

/**
 * @brief Delay function, actual test is 5us.
 */
static void ct_soft_i2c_Delay(void) {
    _nop_();
}

/**
 * @brief Initializes I2C communication.
 */
void ct_soft_i2c_Init(void) {
    SDA_SetMode(CT_GPIO_MODE_OUT_PP);
    SCL_SetMode(CT_GPIO_MODE_OUT_PP);
    SCL_out(HIGH);
    ct_soft_i2c_Delay();
    SDA_out(HIGH);
    ct_soft_i2c_Delay();
}

/**
 * @brief Sends the start condition for I2C communication.
 */
static void ct_soft_i2c_Start(void) {
    SDA_out(HIGH);
    _nop_();
    _nop_();
    SCL_out(HIGH);
    ct_soft_i2c_Delay();    // Start condition setup time greater than 4.7us delay
    SDA_out(LOW);
    ct_soft_i2c_Delay();    // Send start signal
    SCL_out(LOW);           // Hold I2C bus, ready to send or receive data
    ct_soft_i2c_Delay();
}

/**
 * @brief Sends the stop condition for I2C communication.
 */
static void ct_soft_i2c_Stop(void) {
    SDA_out(LOW);
    _nop_();
    _nop_();
    SCL_out(HIGH);
    ct_soft_i2c_Delay();
    SDA_out(HIGH);
    ct_soft_i2c_Delay();
}

/**
 * @brief Sends a non-acknowledgment signal.
 * @param _ack Acknowledgment I2C_Status_t.
 *
 * @details
 * ----After the data reading is completed, an acknowledgment bit is sent,
 * ----0->ack, acknowledgment, telling the slave device that I want to continue
 * reading the next byte, the slave device continues to send data after receiving this
 * signal
 * ----1->not ack, no acknowledgment, telling the slave device that I will not
 * continue to receive data, the slave device stops sending data
 */
static void ct_soft_i2c_SendNotAck(I2C_Status_t _ack) {
    // After receiving, pull up SDA, send non-acknowledgment signal
    SDA_out(_ack);
    ct_soft_i2c_Delay();
    SCL_out(HIGH);
    ct_soft_i2c_Delay();
    SCL_out(LOW);    // Pull down, complete the acknowledgment bit
    ct_soft_i2c_Delay();
    SDA_out(HIGH);
    ct_soft_i2c_Delay();
}

/**
 * @brief Waits for an acknowledgment signal.
 *
 * @return uint8_t - Returns 0 if ACK was received, 1 if no ACK was received.
 * @details
 * ----After the master device has finished sending a byte of data,
 * it pulls the SDA line high and waits for the acknowledgment signal from the slave
 * device. If the slave device does not acknowledge, the SDA line will remain high. In
 * this case, the while loop waits for 250 times. If the SDA line is pulled low by the
 * slave device during the high level of the SCL line, it indicates that the slave
 * device has acknowledged, and the while loop is skipped, marking the end of the
 * acknowledgment.
 */
static uint8_t ct_soft_i2c_WaitAck(void) {
    uint8_t timeout = 250;    // Set a timeout value to prevent infinite loops

    SDA_out(HIGH);    // Release SDA line
    ct_soft_i2c_Delay();
    SCL_out(HIGH);    // Pull SCL high to request ACK
    ct_soft_i2c_Delay();

    while (SDA_ReadPin() && --timeout)
        ;            // Wait for ACK or timeout
    SCL_out(LOW);    // Pull SCL low to end the ACK bit
    // SDA_out(HIGH);
    return (timeout == 0) ? 1 : 0;    // Return 1 if timeout occurred (no ACK), else 0
}

/**
 * @brief Sends a byte of data.
 * @param _byte Data byte to send.
 *
 * @details
 * ----Writes a byte, from high bit to low bit.
 * ----The master device sends data to the slave device,
 * and the slave device collects the SDA data at the falling edge of the clock.
 */
static void ct_soft_i2c_SendByte(uint8_t _byte) {
    uint8_t i, pin_val;
    SCL_out(LOW);    // Falling edge collects data
    for (i = 0; i < 8; i++) {
        ct_soft_i2c_Delay();
        pin_val = (_byte & 0X80) ? 1 : 0;
        SDA_out(pin_val);
        _byte = _byte << 1;
        SCL_out(HIGH);    // Set the clock line to high, notify the controller
                          // to start receiving data bits
        ct_soft_i2c_Delay();
        SCL_out(LOW);
    }
    SDA_out(HIGH);    // After sending, release the data line, check the
                      // slave's acknowledgment
}

/**
 * @brief Writes a byte of data to a specific address on a specific slave device.
 *        Stops and returns if an error occurs during the write process.
 * @param _slave_addr Address of the slave device.
 * @param _byte Data byte to write.
 * @return uint8_t - Returns 0 if write was successful, non-zero error code otherwise.
 *
 * @details
 * ----If there are multiple slave devices,
 * an additional parameter can be added to write the address of the slave device.
 */
uint8_t ct_soft_i2c_WriteByte(uint8_t _slave_addr, uint8_t _byte) {
    uint8_t ack;

    ct_soft_i2c_Start();
    ct_soft_i2c_SendByte(_slave_addr);
    ack = ct_soft_i2c_WaitAck();
    if (ack) {
        ct_soft_i2c_Stop();
        return ack;
    }

    ct_soft_i2c_SendByte(_byte);
    ack = ct_soft_i2c_WaitAck();
    if (ack) {
        ct_soft_i2c_Stop();
        return ack;
    }

    ct_soft_i2c_Stop();
    return 0;    // Write was successful
}

/**
 * @brief Continuously writes data to a specific address and checks for ACK after each
 * byte.
 * @param i2c_data Struct containing buffer and length of data to write.
 * @param _slave_addr Address to write data to.
 * @param _reg Register to write data to.
 * @retval None
 */
void ct_soft_i2c_Write_8Reg(soft_i2c_buffer_t *i2c_data, uint8_t _slave_addr, uint8_t _reg) {
    uint8_t ack;
    uint8_t length = i2c_data->length;
    if (length > max_size) {
        i2c_data->error = CT_I2C_BUFFER_OVERFLOW;
        return;
    }
    ct_soft_i2c_Start();                  // Start I2C communication
    ct_soft_i2c_SendByte(_slave_addr);    // Send slave address with write operation
    ack = ct_soft_i2c_WaitAck();          // Wait for ACK from slave
    if (ack) {
        ct_soft_i2c_Stop();
        i2c_data->error = CT_I2C_MSTR_TX_ADDR_NACK;    // Set error if no ACK received
        return;
    }

    ct_soft_i2c_SendByte(_reg);     // Send register address
    ack = ct_soft_i2c_WaitAck();    // Wait for ACK from slave
    if (ack) {
        ct_soft_i2c_Stop();
        i2c_data->error = CT_I2C_MSTR_TX_ADDR_NACK;    // Set error if no ACK received
        return;
    }

    while (length) {                                    // While there is data to write
        ct_soft_i2c_SendByte(*(i2c_data->buffer++));    // Send a byte from buffer
        ack = ct_soft_i2c_WaitAck();                    // Wait for ACK from slave
        if (ack) {
            ct_soft_i2c_Stop();
            i2c_data->error = CT_I2C_MSTR_TX_DATA_NACK;    // Set error if no ACK received
            return;
        }
        length--;    // Decrement length
        ct_soft_i2c_Delay();
    }
    ct_soft_i2c_Stop();     // Stop I2C communication
    i2c_data->error = 0;    // Set error to 0 to indicate success
}

/**
 * @brief Write data to a 16-bit register over I2C.
 *
 * @param i2c_data Pointer to a soft_i2c_buffer_t structure that contains the data to be
 * written and the length of the data.
 * @param _slave_addr The 7-bit I2C slave address of the target device.
 * @param _reg The 16-bit register address where the data will be written.
 * @retval None
 */
void ct_soft_i2c_Write_16Reg(soft_i2c_buffer_t *i2c_data, uint8_t _slave_addr, uint16_t _reg) {
    uint8_t ack;
    uint8_t length   = i2c_data->length;
    uint8_t reg_high = (_reg >> 8) & 0xFF;    // Extract high byte of 16-bit address
    uint8_t reg_low  = _reg & 0xFF;           // Extract low byte of 16-bit address

    ct_soft_i2c_Start();                  // Start I2C communication
    ct_soft_i2c_SendByte(_slave_addr);    // Send slave address with write operation
    ack = ct_soft_i2c_WaitAck();          // Wait for ACK from slave
    if (ack) {
        ct_soft_i2c_Stop();
        i2c_data->error = CT_I2C_MSTR_TX_ADDR_NACK;    // Set error if no ACK received
        return;
    }

    ct_soft_i2c_SendByte(reg_high);    // Send high byte of register address
    ack = ct_soft_i2c_WaitAck();       // Wait for ACK from slave
    if (ack) {
        ct_soft_i2c_Stop();
        i2c_data->error = CT_I2C_MSTR_TX_ADDR_NACK;    // Set error if no ACK received
        return;
    }

    ct_soft_i2c_SendByte(reg_low);    // Send low byte of register address
    ack = ct_soft_i2c_WaitAck();      // Wait for ACK from slave
    if (ack) {
        ct_soft_i2c_Stop();
        i2c_data->error = CT_I2C_MSTR_TX_ADDR_NACK;    // Set error if no ACK received
        return;
    }

    while (length) {                                    // While there is data to write
        ct_soft_i2c_SendByte(*(i2c_data->buffer++));    // Send a byte from buffer
        ack = ct_soft_i2c_WaitAck();                    // Wait for ACK from slave
        if (ack) {
            ct_soft_i2c_Stop();
            i2c_data->error = CT_I2C_MSTR_TX_DATA_NACK;    // Set error if no ACK received
            return;
        }
        length--;    // Decrement length
        ct_soft_i2c_Delay();
    }
    ct_soft_i2c_Stop();     // Stop I2C communication
    i2c_data->error = 0;    // Set error to 0 to indicate success
}

/**
 * @brief Receives a byte of data.
 * @return Received data byte.
 *
 * @details
 * ----Reads a byte, the high bit is read first, so it needs to be shifted to the
 * left.
 * ----The master device reads data from the slave device. Essentially, the slave
 * device detects that there are 8 rising edges in the clock signal sent by the master
 * device.
 */
static uint8_t ct_soft_i2c_ReceiveByte(void) {
    uint8_t i, pin_val;
    uint8_t read_byte = 0;
    SDA_out(HIGH);    // Make sure the master releases SDA first
    SCL_out(LOW);     // Pull down, collect data
    ct_soft_i2c_Delay();
    for (i = 0; i < 8; i++) {
        pin_val   = SDA_ReadPin();
        read_byte = (read_byte << 1) | pin_val;
        SCL_out(HIGH);
        ct_soft_i2c_Delay();
        SCL_out(LOW);
        ct_soft_i2c_Delay();
    }
    return read_byte;
}

/**
 * @brief Reads a byte of data from a specific address with error checking.
 * @param result Pointer to a soft_i2c_t struct where the read data and error status will
 * be stored.
 * @param _slave_addr I2C slave device address.
 * @return void
 */
uint8_t ct_soft_i2c_ReadByte(soft_i2c_single_t *result, uint8_t _slave_addr) {
    uint8_t ack;
    if (result == NULL) return READ_DATA_ERROR;    // Check if the result pointer is valid

    ct_soft_i2c_Start();
    ct_soft_i2c_SendByte(_slave_addr);    // Send device address + write signal
    ack = ct_soft_i2c_WaitAck();
    if (ack) {
        ct_soft_i2c_Stop();
        result->error = CT_I2C_MSTR_RX_ADDR_NACK;
        return READ_DATA_ERROR;
    }

    ct_soft_i2c_Start();                      // Must restart IIC
    ct_soft_i2c_SendByte(_slave_addr + 1);    // Send device address + read signal
    ack = ct_soft_i2c_WaitAck();
    if (ack) {
        ct_soft_i2c_Stop();
        result->error = CT_I2C_MSTR_RX_ADDR_NACK;
        return READ_DATA_ERROR;
    }

    result->byte = ct_soft_i2c_ReceiveByte();    // Read out data
    ct_soft_i2c_SendNotAck(NOT_ACK);             // Send non-acknowledgment signal
    /*
    If it is followed by I2CStop, send a non-acknowledgment, telling the slave not to
    send data anymore, note that there is no continuous reading here Otherwise, the
    slave will continue to send data after receiving the 0 acknowledgment bit, but the
    master does not receive it at this time, causing the data to only be sent
    successfully for the first time, and no data can be received later.
    */
    ct_soft_i2c_Stop();
    result->error = CT_I2C_MSTR_RX_DATA_ACK;    // No error
    return result->byte;
}

/**
 * @brief Continuously reads multiple bytes from a given I2C slave device starting from a
 * specified register.
 *
 * @param i2c_data Pointer to a `soft_i2c_t` structure that contains the buffer for
 * storing read data, the number of bytes to read, and a variable to store any error
 * status.
 * @param _slave_addr The 7-bit I2C address of the slave device.
 * @param _reg The register address from which the read operation should start.
 *
 * @note The caller must ensure that the buffer in `i2c_data` is large enough to hold the
 * number of bytes specified.
 */
void ct_soft_i2c_Read_8Reg(soft_i2c_buffer_t *i2c_data, uint8_t _slave_addr, uint8_t _reg) {
    uint8_t i;
    if (!i2c_data || !i2c_data->buffer) {
        // Handle invalid i2c_data or buffer pointer
        return;
    }

    i2c_data->error = CT_I2C_MSTR_RX_DATA_ACK;    // Initialize error as success

    ct_soft_i2c_Start();
    ct_soft_i2c_SendByte(_slave_addr);
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    ct_soft_i2c_SendByte(_reg);
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    ct_soft_i2c_Start();
    ct_soft_i2c_SendByte(_slave_addr + 1);
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    for (i = 0; i < i2c_data->length; i++) {
        i2c_data->buffer[i] = ct_soft_i2c_ReceiveByte();
        // Send ACK for all but the last byte
        ct_soft_i2c_SendNotAck((i == (i2c_data->length - 1)) ? NOT_ACK : ACK);
    }

    ct_soft_i2c_Stop();
}

/**
 * @brief Read data from a 16-bit register address over I2C.
 *
 * @param i2c_data Pointer to a soft_i2c_buffer_t structure that contains the buffer
 *                 into which the read data will be stored, along with the length of
 *                 the data to be read.
 * @param _slave_addr The 7-bit I2C slave address of the device from which to read.
 *                    The address should be right-aligned, without the read/write bit.
 * @param _reg The 16-bit register address from which to start reading.
 *
 * @note This function assumes that the lower-level I2C functions such as
 * ct_soft_i2c_Start, ct_soft_i2c_SendByte, etc., are implemented correctly and handle the
 * I2C protocol details.
 * @retval None
 */
void ct_soft_i2c_Read_16Reg(soft_i2c_buffer_t *i2c_data, uint8_t _slave_addr, uint16_t _reg) {
    uint8_t i;
    if (!i2c_data || !i2c_data->buffer) {
        // Handle invalid i2c_data or buffer pointer
        return;
    }

    i2c_data->error = CT_I2C_MSTR_RX_DATA_ACK;    // Initialize error as success

    ct_soft_i2c_Start();
    ct_soft_i2c_SendByte(_slave_addr);
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    // Send the high byte of the 16-bit register address
    ct_soft_i2c_SendByte((uint8_t)(_reg >> 8));
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    // Send the low byte of the 16-bit register address
    ct_soft_i2c_SendByte((uint8_t)(_reg & 0xFF));
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    ct_soft_i2c_Start();
    ct_soft_i2c_SendByte(_slave_addr | 0x01);    // Read operation (slave address ORed with 1)
    if (ct_soft_i2c_WaitAck()) {
        i2c_data->error = CT_I2C_MSTR_RX_ADDR_NACK;
        ct_soft_i2c_Stop();
        return;
    }

    for (i = 0; i < i2c_data->length; i++) {
        i2c_data->buffer[i] = ct_soft_i2c_ReceiveByte();
        // Send ACK for all but the last byte
        ct_soft_i2c_SendNotAck((i == (i2c_data->length - 1)) ? NOT_ACK : ACK);
    }

    ct_soft_i2c_Stop();
}